package controller.validator;

import java.util.ArrayList;
import java.util.Date;
import java.util.NoSuchElementException;

import manager.BidMgr;
import manager.CompletedCourseMgr;
import manager.CourseMgr;
import manager.CoursePreRequisiteMgr;
import manager.RoundMgr;
import manager.SectionMgr;
import manager.SectionStudentMgr;
import manager.StudentMgr;
import util.DateUtil;
import util.ValidationUtil;
import controller.RoundCtrl;
import controller.SectionStudentCtrl;
import entity.Bid;
import entity.Course;
import entity.Round;
import entity.Section;
import entity.Student;
import exception.InputValidationErrors;

/**
 * An abstract class that exposes methods for performing all validations.
 * Concrete classes implement how validation is done before adding, updating or
 * deleting records in the database.
 * 
 * @author jaslin.wee.2012, huangjm.2012, huiwensong.2012
 * 
 */

public abstract class Validator {

	public static final String ALREADY_COMPLETED = "already completed";
	public static final String BID_TOO_LOW = "bid too low";
	public static final String CLASS_TIMETABLE_CLASH = "class timetable clash";
	public static final String COURSE_COMPLETED = "course completed";
	public static final String COURSE_ENROLLED = "course enrolled";
	public static final String DUPLICATE_USERID = "duplicate userid";
	public static final String EXAM_TIMETABLE_CLASH = "exam timetable clash";
	public static final String INCOMPLETE_PREREQUISITES = "incomplete prerequisites";
	public static final String INSUFFICIENT_E$ = "insufficient e$";
	public static final String INVALID_AMOUNT = "invalid amount";
	public static final String INVALID_CODE = "invalid code";
	public static final String INVALID_COURSE = "invalid course";
	public static final String INVALID_COURSE_COMPLETED = "invalid course completed";
	public static final String INVALID_DAY = "invalid day";
	public static final String INVALID_EDOLLAR = "invalid e-dollar";
	public static final String INVALID_END = "invalid end";
	public static final String INVALID_EXAM_DATE = "invalid exam date";
	public static final String INVALID_EXAM_END = "invalid exam end";
	public static final String INVALID_EXAM_START = "invalid exam start";
	public static final String INVALID_PREREQUISITE = "invalid prerequisite";
	public static final String INVALID_SECTION = "invalid section";
	public static final String INVALID_SIZE = "invalid size";
	public static final String INVALID_START = "invalid start";
	public static final String INVALID_USERID = "invalid userid";
	public static final String NO_SUCH_BID = "no such bid";
	public static final String NO_SUCH_ENROLLMENT_RECORD = "no such enrollment record";
	public static final String NO_VACANCY = "no vacancy";
	public static final String NOT_ENOUGH_EDOLLAR = "not enough e-dollar";
	public static final String NOT_OWN_SCHOOL_COURSE = "not own school course";
	public static final String ROUND_2_ENDED = "round 2 ended";
	public static final String ROUND_ALREADY_ENDED = "round already ended";
	public static final String ROUND_ALREADY_STARTED = "round already started";
	public static final String ROUND_ENDED = "round ended";
	public static final String ROUND_NOT_STARTED = "round not started";
	public static final String SECTION_LIMIT_REACHED = "section limit reached";

	BidMgr bidMgr;
	CompletedCourseMgr completedCourseMgr;
	CourseMgr courseMgr;
	CoursePreRequisiteMgr coursePreRequisiteMgr;
	RoundCtrl roundCtrl;
	RoundMgr roundMgr;
	SectionMgr sectionMgr;
	SectionStudentMgr sectionStudentMgr;
	StudentMgr studentMgr;
	SectionStudentCtrl sectionStudentCtrl;

	public Validator() {
		studentMgr = new StudentMgr();
		courseMgr = new CourseMgr();
		sectionMgr = new SectionMgr();
		bidMgr = new BidMgr();
		coursePreRequisiteMgr = new CoursePreRequisiteMgr();
		completedCourseMgr = new CompletedCourseMgr();
		roundMgr = new RoundMgr();
		sectionStudentMgr = new SectionStudentMgr();
		roundCtrl = new RoundCtrl();
		sectionStudentCtrl = new SectionStudentCtrl();
	}

	/**
	 * Checks if a course and section exists for the given course code and
	 * section code respectively.
	 * 
	 * @param courseCode
	 * @param sectionCode
	 * @return
	 * @throws Exception
	 *             {@link #INVALID_COURSE} if a course doesn't exist,
	 *             {@link #INVALID_SECTION} if a section doesn't exist;
	 *             exception is thrown in this order
	 */
	public Section checkForCourseAndSection(String courseCode,
			String sectionCode) throws Exception {
		/*
		 * Validation for invalid course & invalid section
		 */
		try {
			checkForCourse(courseCode);
		} catch (Exception e) {
			throw new Exception(Validator.INVALID_COURSE);
		}

		Section section = null;
		try {
			section = checkForSection(courseCode, sectionCode);
		} catch (Exception e) {
			throw new Exception(Validator.INVALID_SECTION);
		}

		return section;
	}

	/**
	 * Checks if the given student has completed prerequisites for the given
	 * course.
	 * 
	 * @param userId
	 * @param courseCode
	 * @return course for the given course code
	 * @throws IllegalArgumentException
	 *             if student has not completed all prerequisites for the given
	 *             course
	 */
	public Course checkForPreRequisitesCompletion(String userId,
			String courseCode) throws Exception {
		ArrayList<Course> prerequisitesForCourse = coursePreRequisiteMgr
				.retrievePreRequisitesForCourse(courseCode);
		ArrayList<Course> completedCourseForStudent = completedCourseMgr
				.retrieveCompletedCoursesForStudent(userId);
		if (!prerequisitesForCourse.isEmpty()
				&& !completedCourseForStudent
						.containsAll(prerequisitesForCourse)) {
			throw new IllegalArgumentException("prerequisites for "
					+ courseCode + " is not completed");
		}
		return courseMgr.retrieveCourseForCourseCode(courseCode);
	}

	/**
	 * Checks if a bid for the given user and course code already exists.
	 * 
	 * @param userId
	 * @param courseCode
	 * @throws IllegalArgumentException
	 *             if a prior bid exists
	 */
	public void checkForPriorBid(String userId, String courseCode)
			throws Exception {
		try {
			bidMgr.retrieveBid(courseCode, userId);
			throw new IllegalArgumentException("a prior bid exists");
		} catch (NoSuchElementException e) {

		}
	}

	/**
	 * Checks if the course for the given course code has already been completed
	 * by the student
	 * 
	 * @param userId
	 * @param courseCode
	 * @return a course object for the given course code
	 * @throws IllegalArgumentException
	 *             if the course has already been completed
	 */
	public Course checkForPriorCourseCompletion(String userId, String courseCode)
			throws Exception {

		Course course = courseMgr.retrieveCourseForCourseCode(courseCode);

		ArrayList<Course> previouslyCompletedCourses = completedCourseMgr
				.retrieveCompletedCoursesForStudent(userId);

		if (previouslyCompletedCourses.contains(course)) {
			throw new IllegalArgumentException(courseCode
					+ " is already completed");
		}

		return course;
	}

	/**
	 * Checks if the given course is currently enrolled by student.
	 * 
	 * @param course
	 * @param userId
	 * @throws IllegalArgumentException
	 *             if the course is currently enrolled by student
	 */
	public Course checkForPriorCourseEnrollment(Course course, String userId)
			throws Exception {

		ArrayList<Course> currentlyEnrolledCourses = sectionStudentMgr
				.retrieveCourseEnrollmentsForStudent(userId);

		if (currentlyEnrolledCourses.contains(course)) {
			throw new IllegalArgumentException(course.getCourseCode()
					+ " is already enrolled");
		}

		return course;
	}

	/**
	 * Checks if the given section has available vacancy.
	 * 
	 * @param section
	 * @return the number of available vacancy
	 */
	public int checkForVacancy(Section section) throws Exception {
		int vacancy = sectionStudentCtrl.computeVacancyForSection(section);
		if (vacancy < 1) {
			throw new IllegalArgumentException("no vacancy available");
		}

		return vacancy;
	}

	/**
	 * Checks if there exists a bid for the given student, course code and
	 * section code.
	 * 
	 * @param userId
	 * @param courseCode
	 * @param sectionCode
	 * @return a bid object if a prior bid exists
	 * @throws NoSuchElementException
	 *             if a bid doesn't exist
	 */
	public Bid checkForBid(String userId, String courseCode, String sectionCode)
			throws Exception {
		return bidMgr.retrieveBid(courseCode, sectionCode, userId);
	}

	/**
	 * Checks if there exists a bid for the given student, course code.
	 * 
	 * @param userId
	 * @param courseCode
	 * @return a bid object if a prior bid exists
	 * @throws NoSuchElementException
	 *             if a bid doesn't exist
	 */
	public Bid checkForBid(String userId, String courseCode) throws Exception {
		return bidMgr.retrieveBid(courseCode, userId);
	}

	/**
	 * Checks if a course exists for the given course code.
	 * 
	 * @param courseCode
	 * @return a course object for the given course code if it exists
	 * @throws NoSuchElementException
	 *             if a course doesn't exists for the given course code
	 */
	public Course checkForCourse(String courseCode) throws Exception {
		return courseMgr.retrieveCourseForCourseCode(courseCode);
	}

	/**
	 * Checks if the given date string conforms with the desired input date
	 * format.
	 * 
	 * @param dateStr
	 * @return the given date in the Date type
	 * @throws IllegalArgumentException
	 *             if the date format mismatch the desired input date pattern
	 */
	public Date checkForInputDateFormat(String dateStr) throws Exception {
		return ValidationUtil.checkForDateFormat(dateStr,
				DateUtil.INPUT_DATE_FORMAT, DateUtil.INPUT_DATE_REGEX);
	}

	/**
	 * Checks if another student already exists for the given userId. Not to be
	 * confused with {@link #checkForStudent(String)}.
	 * 
	 * @param userId
	 * @throws IllegalArgumentException
	 *             if the student already exists (i.e. duplicate student)
	 */
	public void checkForDuplicateStudent(String userId) throws Exception {
		try {
			studentMgr.retrieveStudentForUserId(userId);
		} catch (NoSuchElementException e) {
			// no such student for the given userId, YAY! =)
			return;
		}

		throw new IllegalArgumentException(userId + " already exists");
	}

	/**
	 * Checks if the student has enough e-dollar when updating a <b>PREVIOUS</b>
	 * bid
	 * 
	 * @param previousBid
	 * @param newBidAmount
	 * @return the bid amount to deduct from student
	 * @throws IllegalArgumentException
	 *             if the student does not have enough e-dollar
	 */
	public double checkForEnoughEdollar(Bid previousBid, double newBidAmount)
			throws Exception {
		double bidAmountToDeduct = newBidAmount - previousBid.getBidAmount();
		if (previousBid.getStudent().getEdollar() - bidAmountToDeduct < 0) {
			throw new IllegalArgumentException("not enough e-dollar");
		}
		return bidAmountToDeduct;
	}

	/**
	 * Checks if the mid amount meets minimum value
	 * 
	 * @param minAmount
	 * @param bidAmount
	 * @return the bid amount to deduct from student
	 * @throws IllegalArgumentException
	 *             if the student bid too low
	 */
	public double checkForEnoughEdollar(double minAmount, double bidAmount)
			throws Exception {
		ValidationUtil.checkForMinValue(bidAmount, minAmount);
		return bidAmount;
	}

	/**
	 * Checks if the student has enough e-dollar for a <b>NEW</b> bid
	 * 
	 * @param student
	 * @param bidAmount
	 * @return the bid amount to deduct from student
	 * @throws IllegalArgumentException
	 *             if student does not have enough e-dollar
	 */
	public double checkForEnoughEdollar(Student student, double bidAmount)
			throws Exception {
		if (student.getEdollar() < bidAmount) {
			throw new IllegalArgumentException("not enough e-dollar");
		}
		return bidAmount;
	}

	/**
	 * Checks if the given course's exam timetable clashes with the student's
	 * previously bid courses.
	 * 
	 * @param course
	 * @param userId
	 * @return back the given course object if there's no clash
	 * @throws IllegalArgumentException
	 *             if there's an exam timetable clash with any of the previously
	 *             bid courses
	 */
	public Course checkForExamTimetableClashWithPreviousBidsIgnoringSameCourse(Course course,
			String userId) throws Exception {

		ArrayList<Bid> previousBids = bidMgr.retrieveAllBidsForStudent(userId);

		for (Bid eachPreviousBid : previousBids) {

			Course eachPreviouslyBidCourse = eachPreviousBid.getSection()
					.getCourse();

			if (!eachPreviouslyBidCourse.equals(course)) {
				ValidationUtil.checkForDateClash(course.getExamStart(),
						course.getExamEnd(),
						eachPreviouslyBidCourse.getExamStart(),
						eachPreviouslyBidCourse.getExamEnd());
				// will throw an IllegalArgumentException if there's a clash
			}
		}

		return course;
	}

	/**
	 * Checks if the given bid amount satisfies the minimum bid amount for the
	 * current round.
	 * 
	 * @param bidAmount
	 * @param section
	 *            for which the bid amount is trying to bid
	 * @return back the bid amount
	 * @throws IllegalArgumentException
	 *             if the bid amount does not meet the minimum
	 */
	public double checkForMinBidAmountForSection(double bidAmount,
			Section section) {
		double minBidAmountForCurrentRound = roundCtrl
				.getMinBidAmountForSection(section);

		ValidationUtil.checkForMinValue(bidAmount, minBidAmountForCurrentRound);

		return bidAmount;
	}

	/**
	 * Checks if the given course is from the same school as the given student.
	 * 
	 * @param course
	 * @param student
	 * @return back the course object
	 * @throws IllegalArgumentException
	 *             if course is not from the student's school
	 */
	public Course checkForOwnSchoolCourseInRoundOne(Course course,
			Student student) throws Exception {
		if (roundCtrl.isCurrentRoundNo(Round.ROUND_ONE)
				&& !course.getSchool().equalsIgnoreCase(student.getSchool())) {
			throw new IllegalArgumentException(course.getCourseCode()
					+ " offered by " + course.getSchool() + " is not from "
					+ student.getSchool());
		}
		return course;
	}

	/**
	 * Checks if current round round one.
	 * 
	 * @return a round object for the current round
	 * @throws Exception
	 *             if the current round is not round one
	 */
	public void checkForRoundOne() throws Exception {
		if (roundCtrl.isCurrentRoundNo(Round.ROUND_ONE)) {
			throw new Exception("current round is not round one");
		}
	}

	/**
	 * Checks if a round is open.
	 * 
	 * @return a round object for the current round
	 * @throws Exception
	 *             if the current round is closed
	 */
	public Round checkForRoundOpening() throws Exception {
		Round currentRound = roundMgr.retrieveCurrentRound();
		if (!currentRound.isRoundOpen()) {
			throw new Exception("round is closed");
		}

		return currentRound;
	}

	/**
	 * Checks if a section exists for the given course code and section code.
	 * 
	 * @param courseCode
	 * @param sectionCode
	 * @return a section object for the given course code and section code if it
	 *         exists
	 * @throws NoSuchElementException
	 *             if the section doesn't exists
	 */
	public Section checkForSection(String courseCode, String sectionCode)
			throws Exception {
		return sectionMgr.retrieveSectionForCourseCodeAndSectionCode(
				courseCode, sectionCode);
	}

	/**
	 * Checks if the given student has reached the maximum bid limit
	 * 
	 * @param userId
	 * @return the number of bids left before reaching the maximum bid limit
	 * @throws IllegalArgumentException
	 *             if student has reached the maximum bid limit
	 */
	public int checkForSectionBiddingLimit(String userId) throws Exception {
		int maxBid = 5;
		ArrayList<Bid> previousBids = bidMgr.retrieveAllBidsForStudent(userId);
		if (previousBids.size() >= maxBid) {
			throw new IllegalArgumentException(userId
					+ " has reached the bid limit");
		}
		return maxBid - previousBids.size();
	}

	/**
	 * Checks if a section-student enrollment record exists for the given course
	 * code, section code and user id.
	 * 
	 * @param courseCode
	 * @param sectionCode
	 * @param userId
	 * @return a bid object for the section-student enrollment
	 * @throws NoSuchElementException
	 *             if a section-student enrollment record is not found
	 */
	public Bid checkForSectionStudentEnrollment(String courseCode,
			String sectionCode, String userId) throws Exception {
		return sectionStudentMgr.retrieveSectionEnrollmentForStudent(
				courseCode, sectionCode, userId);
	}

	/**
	 * Checks if the timing of the given section clashes with the student's
	 * previously bid sections.
	 * 
	 * @param section
	 * @param userId
	 * @return back the given section if there's no clash
	 * @throws IllegalArgumentException
	 *             if there's a clash with any of the previously bid sections'
	 *             class timetable.
	 */
	public Section checkForSectionTimetableClashWithPreviousBidsIgnoringSameCourse(
			Section section, String userId) throws Exception {

		ArrayList<Bid> previousBids = bidMgr.retrieveAllBidsForStudent(userId);

		for (Bid eachPreviousBid : previousBids) {

			Section eachPreviouslyBidSection = eachPreviousBid.getSection();

			if (!eachPreviouslyBidSection.getCourse().equals(
					section.getCourse())
					&& section.getDayOfWeek() == eachPreviouslyBidSection
							.getDayOfWeek()) {
				// if both sections are on the same day
				// check for timing clash
				ValidationUtil.checkForDateClash(
						eachPreviouslyBidSection.getStartTime(),
						eachPreviouslyBidSection.getEndTime(),
						section.getStartTime(), section.getEndTime());
				// will throw an IllegalArgumentException if there's a clash
			}
		}

		return section;
	}

	/**
	 * Checks if a student for the given userId exists. Not to be confused with
	 * {@link #checkForDuplicateStudent(String)}.
	 * 
	 * @param userId
	 * @return a student object for the given userId
	 * @throws NoSuchElementException
	 *             if the student does not exists
	 */
	public Student checkForStudent(String userId) throws Exception {
		return studentMgr.retrieveStudentForUserId(userId);
	}

	/**
	 * Checks if the given time string conforms with the desired input time
	 * format.
	 * 
	 * @param timeStr
	 * @return the given time in the Date type
	 * @throws IllegalArgumentException
	 *             if the time format mismatch the desired input time pattern
	 */
	public Date checkForInputTimeFormat(String timeStr) throws Exception {
		return ValidationUtil.checkForDateFormat(timeStr,
				DateUtil.INPUT_TIME_FORMAT, DateUtil.INPUT_TIME_REGEX);
	}

	public void checkForValidSectionStartTime(String timeStr) {
		if (!(timeStr.equals("8:30") || timeStr.equals("12:00") || timeStr
				.equals("15:30"))) {
			throw new IllegalArgumentException(timeStr
					+ " is an invalid section start time");
		}
	}

	public abstract void performValidationBeforeAdding()
			throws InputValidationErrors;

	public abstract void performValidationBeforeDeleting()
			throws InputValidationErrors;

	public abstract void performValidationBeforeUpdating()
			throws InputValidationErrors;

	/**
	 * Checks if the given bid amount satisfies the following conditions.
	 * <ol>
	 * <li>It is a valid double</li>
	 * <li>It is at least {@link RoundCtrl#MIN_BID_AMOUNT}</li>
	 * <li>It has not more than 2 decimal places</li>
	 * </ol>
	 * 
	 * @param bidAmountStr
	 * @return bidAmount in the double type
	 * @throws IllegalArgumentException
	 *             if the bid amount string fails the above mentioned tests
	 */
	public double validateBidAmount(String bidAmountStr) throws Exception {

		double bidAmount = ValidationUtil.checkForDouble(bidAmountStr);

		ValidationUtil.checkForMinValue(bidAmount, RoundCtrl.MIN_BID_AMOUNT);

		ValidationUtil.checkForMaxDecimalPlaces(bidAmountStr, 2);

		return bidAmount;
	}

	/**
	 * Checks if the given edollar is a valid double and it is not negative
	 * (i.e. zero is ok).
	 * 
	 * @param edollarStr
	 * @return edollar in the Double type
	 * @throws IllegalArgumentException
	 *             if the given edollar string fails the tests
	 */
	public double validateEdollar(String edollarStr) throws Exception {
		double edollar = ValidationUtil.checkForDouble(edollarStr);
		return ValidationUtil.checkForNonNegetive(edollar);
	}

	/**
	 * Checks if the given section code matches the pattern Sx, where x is an
	 * integer between 1 and 99, inclusive.
	 * 
	 * @param sectionCode
	 * @return the validated sectionCode
	 * @throws IllegalArgumentException
	 *             if the given section code fails the tests
	 */
	public String validateSectionCode(String sectionCode) throws Exception {
		String sectionCodePattern = "S[1-9]{1}[0-9]{0,1}";
		if (sectionCode.matches(sectionCodePattern)) {
			// check the section number portion
			String sectionNoStr = sectionCode
					.substring(1, sectionCode.length());
			int sectionNo = ValidationUtil.checkForInteger(sectionNoStr);
			ValidationUtil.checkForRange(sectionNo, 1, 99);
			return sectionCode;
		}

		throw new IllegalArgumentException(sectionCode
				+ " is not in the desired pattern");
	}

	/**
	 * Checks if the given day is a valid integer between 1 and 7, inclusive.
	 * 
	 * @param dayStr
	 * @return day in Integer type
	 * @throws IllegalArgumentException
	 *             if the given dayStr fails the tests
	 */
	public int validateSectionDay(String dayStr) throws Exception {
		int day = ValidationUtil.checkForInteger(dayStr);
		return ValidationUtil.checkForRange(day, 1, 7);
	}

	/**
	 * Checks if the given size is a valid positive integer
	 * 
	 * @param sizeStr
	 * @return size in Integer type
	 * @throws IllegalArgumentException
	 *             if the give size fails the tests
	 */
	public int validateSectionSize(String sizeStr) throws Exception {
		int size = ValidationUtil.checkForInteger(sizeStr);
		return ValidationUtil.checkForPositive(size);
	}

}
